/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2009 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smack;

import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;

/**
 * The abstract Connection class provides an interface for connections to a XMPP
 * server and implements shared methods which are used by the different types of
 * connections (e.g. XMPPConnection or BoshConnection).
 * 
 * To create a connection to a XMPP server a simple usage of this API might look
 * like the following:
 * 
 * <pre>
 * // Create a connection to the igniterealtime.org XMPP server.
 * Connection con = new XMPPConnection("igniterealtime.org");
 * // Connect to the server
 * con.connect();
 * // Most servers require you to login before performing other tasks.
 * con.login("jsmith", "mypass");
 * // Start a new conversation with John Doe and send him a message.
 * Chat chat = connection.getChatManager().createChat("jdoe@igniterealtime.org"</font>, new MessageListener() {
 * <p/>
 *     public void processMessage(Chat chat, Message message) {
 *         // Print out any messages we get back to standard out.
 *         System.out.println(<font color="green">"Received message: "</font> + message);
 *     }
 * });
 * chat.sendMessage(<font color="green">"Howdy!"</font>);
 * // Disconnect from the server
 * con.disconnect();
 * </pre>
 * <p/>
 * Connections can be reused between connections. This means that an Connection
 * may be connected, disconnected and then connected again. Listeners of the
 * Connection will be retained accross connections.
 * <p>
 * <p/>
 * If a connected Connection gets disconnected abruptly then it will try to
 * reconnect again. To stop the reconnection process, use {@link #disconnect()}.
 * Once stopped you can use {@link #connect()} to manually connect to the
 * server.
 * 
 * @see XMPPConnection
 * @author Matt Tucker
 * @author Guenther Niess
 */
public abstract class Connection {

	/**
	 * Counter to uniquely identify connections that are created.
	 */
	private final static AtomicInteger connectionCounter = new AtomicInteger(0);

	/**
	 * A set of listeners which will be invoked if a new connection is created.
	 */
	private final static Set<ConnectionCreationListener> connectionEstablishedListeners = new CopyOnWriteArraySet<ConnectionCreationListener>();

	/**
	 * Value that indicates whether debugging is enabled. When enabled, a debug
	 * window will apear for each new connection that will contain the following
	 * information:
	 * <ul>
	 * <li>Client Traffic -- raw XML traffic generated by Smack and sent to the
	 * server.
	 * <li>Server Traffic -- raw XML traffic sent by the server to the client.
	 * <li>Interpreted Packets -- shows XML packets from the server as parsed by
	 * Smack.
	 * </ul>
	 * <p/>
	 * Debugging can be enabled by setting this field to true, or by setting the
	 * Java system property <tt>smack.debugEnabled</tt> to true. The system
	 * property can be set on the command line such as
	 * "java SomeApp -Dsmack.debugEnabled=true".
	 */
	public static boolean DEBUG_ENABLED = false;

	static {
		// Use try block since we may not have permission to get a system
		// property (for example, when an applet).
		try {
			DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
		} catch (Exception e) {
			// Ignore.
		}
		// Ensure the SmackConfiguration class is loaded by calling a method in
		// it.
		SmackConfiguration.getVersion();
	}

	/**
	 * A collection of ConnectionListeners which listen for connection closing
	 * and reconnection events.
	 */
	protected final Collection<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<ConnectionListener>();

	/**
	 * A collection of PacketCollectors which collects packets for a specified
	 * filter and perform blocking and polling operations on the result queue.
	 */
	protected final Collection<PacketCollector> collectors = new ConcurrentLinkedQueue<PacketCollector>();

	/**
	 * List of PacketListeners that will be notified when a new packet was
	 * received.
	 */
	protected final Map<PacketListener, ListenerWrapper> recvListeners = new ConcurrentHashMap<PacketListener, ListenerWrapper>();

	/**
	 * List of PacketListeners that will be notified when a new packet was sent.
	 */
	protected final Map<PacketListener, ListenerWrapper> sendListeners = new ConcurrentHashMap<PacketListener, ListenerWrapper>();

	/**
	 * List of PacketInterceptors that will be notified when a new packet is
	 * about to be sent to the server. These interceptors may modify the packet
	 * before it is being actually sent to the server.
	 */
	protected final Map<PacketInterceptor, InterceptorWrapper> interceptors = new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();

	/**
	 * The AccountManager allows creation and management of accounts on an XMPP
	 * server.
	 */
	private AccountManager accountManager = null;

	/**
	 * The ChatManager keeps track of references to all current chats.
	 */
	protected ChatManager chatManager = null;

	/**
	 * The SmackDebugger allows to log and debug XML traffic.
	 */
	protected SmackDebugger debugger = null;

	/**
	 * The Reader which is used for the {@see debugger}.
	 */
	protected Reader reader;

	/**
	 * The Writer which is used for the {@see debugger}.
	 */
	protected Writer writer;

	/**
	 * The permanent storage for the roster
	 */
	protected RosterStorage rosterStorage;

	/**
	 * The SASLAuthentication manager that is responsible for authenticating
	 * with the server.
	 */
	protected SASLAuthentication saslAuthentication = new SASLAuthentication(
			this);

	/**
	 * A number to uniquely identify connections that are created. This is
	 * distinct from the connection ID, which is a value sent by the server once
	 * a connection is made.
	 */
	protected final int connectionCounterValue = connectionCounter
			.getAndIncrement();

	/**
	 * Holds the initial configuration used while creating the connection.
	 */
	protected final ConnectionConfiguration config;

	/**
	 * Create a new Connection to a XMPP server.
	 * 
	 * @param configuration
	 *            The configuration which is used to establish the connection.
	 */
	protected Connection(ConnectionConfiguration configuration) {
		config = configuration;
	}

	/**
	 * Returns the configuration used to connect to the server.
	 * 
	 * @return the configuration used to connect to the server.
	 */
	protected ConnectionConfiguration getConfiguration() {
		return config;
	}

	/**
	 * Returns the name of the service provided by the XMPP server for this
	 * connection. This is also called XMPP domain of the connected server.
	 * After authenticating with the server the returned value may be different.
	 * 
	 * @return the name of the service provided by the XMPP server.
	 */
	public String getServiceName() {
		return config.getServiceName();
	}

	/**
	 * Returns the host name of the server where the XMPP server is running.
	 * This would be the IP address of the server or a name that may be resolved
	 * by a DNS server.
	 * 
	 * @return the host name of the server where the XMPP server is running.
	 */
	public String getHost() {
		return config.getHost();
	}

	public String getCapsNode() {
		return config.getCapsNode();
	}

	/**
	 * Returns the port number of the XMPP server for this connection. The
	 * default port for normal connections is 5222. The default port for SSL
	 * connections is 5223.
	 * 
	 * @return the port number of the XMPP server.
	 */
	public int getPort() {
		return config.getPort();
	}

	/**
	 * Returns the full XMPP address of the user that is logged in to the
	 * connection or <tt>null</tt> if not logged in yet. An XMPP address is in
	 * the form username@server/resource.
	 * 
	 * @return the full XMPP address of the user logged in.
	 */
	public abstract String getUser();

	/**
	 * Returns the connection ID for this connection, which is the value set by
	 * the server when opening a XMPP stream. If the server does not set a
	 * connection ID, this value will be null. This value will be <tt>null</tt>
	 * if not connected to the server.
	 * 
	 * @return the ID of this connection returned from the XMPP server or
	 *         <tt>null</tt> if not connected to the server.
	 */
	public abstract String getConnectionID();

	/**
	 * Returns true if currently connected to the XMPP server.
	 * 
	 * @return true if connected.
	 */
	public abstract boolean isConnected();

	/**
	 * Returns true if currently authenticated by successfully calling the login
	 * method.
	 * 
	 * @return true if authenticated.
	 */
	public abstract boolean isAuthenticated();

	/**
	 * Returns true if currently authenticated anonymously.
	 * 
	 * @return true if authenticated anonymously.
	 */
	public abstract boolean isAnonymous();

	/**
	 * Returns true if the connection to the server has successfully negotiated
	 * encryption.
	 * 
	 * @return true if a secure connection to the server.
	 */
	public abstract boolean isSecureConnection();

	/**
	 * Returns if the reconnection mechanism is allowed to be used. By default
	 * reconnection is allowed.
	 * 
	 * @return true if the reconnection mechanism is allowed to be used.
	 */
	protected boolean isReconnectionAllowed() {
		return config.isReconnectionAllowed();
	}

	/**
	 * Returns true if network traffic is being compressed. When using stream
	 * compression network traffic can be reduced up to 90%. Therefore, stream
	 * compression is ideal when using a slow speed network connection. However,
	 * the server will need to use more CPU time in order to un/compress network
	 * data so under high load the server performance might be affected.
	 * 
	 * @return true if network traffic is being compressed.
	 */
	public abstract boolean isUsingCompression();

	/**
	 * Establishes a connection to the XMPP server and performs an automatic
	 * login only if the previous connection state was logged (authenticated).
	 * It basically creates and maintains a connection to the server.
	 * <p>
	 * <p/>
	 * Listeners will be preserved from a previous connection if the
	 * reconnection occurs after an abrupt termination.
	 * 
	 * @throws XMPPException
	 *             if an error occurs while trying to establish the connection.
	 */
	public abstract void connect() throws XMPPException;

	/**
	 * Logs in to the server using the strongest authentication mode supported
	 * by the server, then sets presence to available. If the server supports
	 * SASL authentication then the user will be authenticated using SASL if not
	 * Non-SASL authentication will be tried. If more than five seconds (default
	 * timeout) elapses in each step of the authentication process without a
	 * response from the server, or if an error occurs, a XMPPException will be
	 * thrown.
	 * <p>
	 * 
	 * Before logging in (i.e. authenticate) to the server the connection must
	 * be connected.
	 * 
	 * It is possible to log in without sending an initial available presence by
	 * using {@link ConnectionConfiguration#setSendPresence(boolean)}. If this
	 * connection is not interested in loading its roster upon login then use
	 * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}. Finally,
	 * if you want to not pass a password and instead use a more advanced
	 * mechanism while using SASL then you may be interested in using
	 * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}
	 * . For more advanced login settings see {@link ConnectionConfiguration}.
	 * 
	 * @param username
	 *            the username.
	 * @param password
	 *            the password or <tt>null</tt> if using a CallbackHandler.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public void login(String username, String password) throws XMPPException {
		login(username, password, "Smack");
	}

	/**
	 * Logs in to the server using the strongest authentication mode supported
	 * by the server, then sets presence to available. If the server supports
	 * SASL authentication then the user will be authenticated using SASL if not
	 * Non-SASL authentication will be tried. If more than five seconds (default
	 * timeout) elapses in each step of the authentication process without a
	 * response from the server, or if an error occurs, a XMPPException will be
	 * thrown.
	 * <p>
	 * 
	 * Before logging in (i.e. authenticate) to the server the connection must
	 * be connected.
	 * 
	 * It is possible to log in without sending an initial available presence by
	 * using {@link ConnectionConfiguration#setSendPresence(boolean)}. If this
	 * connection is not interested in loading its roster upon login then use
	 * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}. Finally,
	 * if you want to not pass a password and instead use a more advanced
	 * mechanism while using SASL then you may be interested in using
	 * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}
	 * . For more advanced login settings see {@link ConnectionConfiguration}.
	 * 
	 * @param username
	 *            the username.
	 * @param password
	 *            the password or <tt>null</tt> if using a CallbackHandler.
	 * @param resource
	 *            the resource.
	 * @throws XMPPException
	 *             if an error occurs.
	 * @throws IllegalStateException
	 *             if not connected to the server, or already logged in to the
	 *             serrver.
	 */
	public abstract void login(String username, String password, String resource)
			throws XMPPException;

	/**
	 * Logs in to the server anonymously. Very few servers are configured to
	 * support anonymous authentication, so it's fairly likely logging in
	 * anonymously will fail. If anonymous login does succeed, your XMPP address
	 * will likely be in the form "123ABC@server/789XYZ" or "server/123ABC"
	 * (where "123ABC" and "789XYZ" is a random value generated by the server).
	 * 
	 * @throws XMPPException
	 *             if an error occurs or anonymous logins are not supported by
	 *             the server.
	 * @throws IllegalStateException
	 *             if not connected to the server, or already logged in to the
	 *             serrver.
	 */
	public abstract void loginAnonymously() throws XMPPException;

	/**
	 * Sends the specified packet to the server.
	 * 
	 * @param packet
	 *            the packet to send.
	 */
	public abstract void sendPacket(Packet packet);

	/**
	 * Returns an account manager instance for this connection.
	 * 
	 * @return an account manager for this connection.
	 */
	public AccountManager getAccountManager() {
		if (accountManager == null) {
			accountManager = new AccountManager(this);
		}
		return accountManager;
	}

	/**
	 * Returns a chat manager instance for this connection. The ChatManager
	 * manages all incoming and outgoing chats on the current connection.
	 * 
	 * @return a chat manager instance for this connection.
	 */
	public synchronized ChatManager getChatManager() {
		if (this.chatManager == null) {
			this.chatManager = new ChatManager(this);
		}
		return this.chatManager;
	}

	/**
	 * Returns the roster for the user.
	 * <p>
	 * This method will never return <code>null</code>, instead if the user has
	 * not yet logged into the server or is logged in anonymously all modifying
	 * methods of the returned roster object like
	 * {@link Roster#createEntry(String, String, String[])},
	 * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
	 * {@link RosterListener}s will throw an IllegalStateException.
	 * 
	 * @return the user's roster.
	 */
	public abstract Roster getRoster();

	/**
	 * Set the store for the roster of this connection. If you set the roster
	 * storage of a connection you enable support for XEP-0237
	 * (RosterVersioning)
	 * 
	 * @param store
	 *            the store used for roster versioning
	 * @throws IllegalStateException
	 *             if you add a roster store when roster is initializied
	 */
	public abstract void setRosterStorage(RosterStorage storage)
			throws IllegalStateException;

	/**
	 * Returns the SASLAuthentication manager that is responsible for
	 * authenticating with the server.
	 * 
	 * @return the SASLAuthentication manager that is responsible for
	 *         authenticating with the server.
	 */
	public SASLAuthentication getSASLAuthentication() {
		return saslAuthentication;
	}

	/**
	 * Closes the connection by setting presence to unavailable then closing the
	 * connection to the XMPP server. The Connection can still be used for
	 * connecting to the server again.
	 * <p>
	 * <p/>
	 * This method cleans up all resources used by the connection. Therefore,
	 * the roster, listeners and other stateful objects cannot be re-used by
	 * simply calling connect() on this connection again. This is unlike the
	 * behavior during unexpected disconnects (and subsequent connections). In
	 * that case, all state is preserved to allow for more seamless error
	 * recovery.
	 */
	public void disconnect() {
		disconnect(new Presence(Presence.Type.unavailable));
	}

	/**
	 * Closes the connection. A custom unavailable presence is sent to the
	 * server, followed by closing the stream. The Connection can still be used
	 * for connecting to the server again. A custom unavilable presence is
	 * useful for communicating offline presence information such as
	 * "On vacation". Typically, just the status text of the presence packet is
	 * set with online information, but most XMPP servers will deliver the full
	 * presence packet with whatever data is set.
	 * <p>
	 * <p/>
	 * This method cleans up all resources used by the connection. Therefore,
	 * the roster, listeners and other stateful objects cannot be re-used by
	 * simply calling connect() on this connection again. This is unlike the
	 * behavior during unexpected disconnects (and subsequent connections). In
	 * that case, all state is preserved to allow for more seamless error
	 * recovery.
	 * 
	 * @param unavailablePresence
	 *            the presence packet to send during shutdown.
	 */
	public abstract void disconnect(Presence unavailablePresence);

	/**
	 * Adds a new listener that will be notified when new Connections are
	 * created. Note that newly created connections will not be actually
	 * connected to the server.
	 * 
	 * @param connectionCreationListener
	 *            a listener interested on new connections.
	 */
	public static void addConnectionCreationListener(
			ConnectionCreationListener connectionCreationListener) {
		connectionEstablishedListeners.add(connectionCreationListener);
	}

	/**
	 * Removes a listener that was interested in connection creation events.
	 * 
	 * @param connectionCreationListener
	 *            a listener interested on new connections.
	 */
	public static void removeConnectionCreationListener(
			ConnectionCreationListener connectionCreationListener) {
		connectionEstablishedListeners.remove(connectionCreationListener);
	}

	/**
	 * Get the collection of listeners that are interested in connection
	 * creation events.
	 * 
	 * @return a collection of listeners interested on new connections.
	 */
	protected static Collection<ConnectionCreationListener> getConnectionCreationListeners() {
		return Collections
				.unmodifiableCollection(connectionEstablishedListeners);
	}

	/**
	 * Adds a connection listener to this connection that will be notified when
	 * the connection closes or fails. The connection needs to already be
	 * connected or otherwise an IllegalStateException will be thrown.
	 * 
	 * @param connectionListener
	 *            a connection listener.
	 */
	public void addConnectionListener(ConnectionListener connectionListener) {
		if (!isConnected()) {
			throw new IllegalStateException("Not connected to server.");
		}
		if (connectionListener == null) {
			return;
		}
		if (!connectionListeners.contains(connectionListener)) {
			connectionListeners.add(connectionListener);
		}
	}

	/**
	 * Removes a connection listener from this connection.
	 * 
	 * @param connectionListener
	 *            a connection listener.
	 */
	public void removeConnectionListener(ConnectionListener connectionListener) {
		connectionListeners.remove(connectionListener);
	}

	/**
	 * Get the collection of listeners that are interested in connection events.
	 * 
	 * @return a collection of listeners interested on connection events.
	 */
	protected Collection<ConnectionListener> getConnectionListeners() {
		return connectionListeners;
	}

	/**
	 * Creates a new packet collector for this connection. A packet filter
	 * determines which packets will be accumulated by the collector. A
	 * PacketCollector is more suitable to use than a {@link PacketListener}
	 * when you need to wait for a specific result.
	 * 
	 * @param packetFilter
	 *            the packet filter to use.
	 * @return a new packet collector.
	 */
	public PacketCollector createPacketCollector(PacketFilter packetFilter) {
		PacketCollector collector = new PacketCollector(this, packetFilter);
		// Add the collector to the list of active collectors.
		collectors.add(collector);
		return collector;
	}

	/**
	 * Remove a packet collector of this connection.
	 * 
	 * @param collector
	 *            a packet collectors which was created for this connection.
	 */
	protected void removePacketCollector(PacketCollector collector) {
		collectors.remove(collector);
	}

	/**
	 * Get the collection of all packet collectors for this connection.
	 * 
	 * @return a collection of packet collectors for this connection.
	 */
	protected Collection<PacketCollector> getPacketCollectors() {
		return collectors;
	}

	/**
	 * Registers a packet listener with this connection. A packet filter
	 * determines which packets will be delivered to the listener. If the same
	 * packet listener is added again with a different filter, only the new
	 * filter will be used.
	 * 
	 * @param packetListener
	 *            the packet listener to notify of new received packets.
	 * @param packetFilter
	 *            the packet filter to use.
	 */
	public void addPacketListener(PacketListener packetListener,
			PacketFilter packetFilter) {
		if (packetListener == null) {
			throw new NullPointerException("Packet listener is null.");
		}
		ListenerWrapper wrapper = new ListenerWrapper(packetListener,
				packetFilter);
		recvListeners.put(packetListener, wrapper);
	}

	/**
	 * Removes a packet listener for received packets from this connection.
	 * 
	 * @param packetListener
	 *            the packet listener to remove.
	 */
	public void removePacketListener(PacketListener packetListener) {
		recvListeners.remove(packetListener);
	}

	/**
	 * Get a map of all packet listeners for received packets of this
	 * connection.
	 * 
	 * @return a map of all packet listeners for received packets.
	 */
	protected Map<PacketListener, ListenerWrapper> getPacketListeners() {
		return recvListeners;
	}

	/**
	 * Registers a packet listener with this connection. The listener will be
	 * notified of every packet that this connection sends. A packet filter
	 * determines which packets will be delivered to the listener. Note that the
	 * thread that writes packets will be used to invoke the listeners.
	 * Therefore, each packet listener should complete all operations quickly or
	 * use a different thread for processing.
	 * 
	 * @param packetListener
	 *            the packet listener to notify of sent packets.
	 * @param packetFilter
	 *            the packet filter to use.
	 */
	public void addPacketSendingListener(PacketListener packetListener,
			PacketFilter packetFilter) {
		if (packetListener == null) {
			throw new NullPointerException("Packet listener is null.");
		}
		ListenerWrapper wrapper = new ListenerWrapper(packetListener,
				packetFilter);
		sendListeners.put(packetListener, wrapper);
	}

	/**
	 * Removes a packet listener for sending packets from this connection.
	 * 
	 * @param packetListener
	 *            the packet listener to remove.
	 */
	public void removePacketSendingListener(PacketListener packetListener) {
		sendListeners.remove(packetListener);
	}

	/**
	 * Get a map of all packet listeners for sending packets of this connection.
	 * 
	 * @return a map of all packet listeners for sent packets.
	 */
	protected Map<PacketListener, ListenerWrapper> getPacketSendingListeners() {
		return sendListeners;
	}

	/**
	 * Process all packet listeners for sending packets.
	 * 
	 * @param packet
	 *            the packet to process.
	 */
	protected void firePacketSendingListeners(Packet packet) {
		// Notify the listeners of the new sent packet
		for (ListenerWrapper listenerWrapper : sendListeners.values()) {
			listenerWrapper.notifyListener(packet);
		}
	}

	/**
	 * Registers a packet interceptor with this connection. The interceptor will
	 * be invoked every time a packet is about to be sent by this connection.
	 * Interceptors may modify the packet to be sent. A packet filter determines
	 * which packets will be delivered to the interceptor.
	 * 
	 * @param packetInterceptor
	 *            the packet interceptor to notify of packets about to be sent.
	 * @param packetFilter
	 *            the packet filter to use.
	 */
	public void addPacketInterceptor(PacketInterceptor packetInterceptor,
			PacketFilter packetFilter) {
		if (packetInterceptor == null) {
			throw new NullPointerException("Packet interceptor is null.");
		}
		interceptors.put(packetInterceptor, new InterceptorWrapper(
				packetInterceptor, packetFilter));
	}

	/**
	 * Removes a packet interceptor.
	 * 
	 * @param packetInterceptor
	 *            the packet interceptor to remove.
	 */
	public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
		interceptors.remove(packetInterceptor);
	}

	public boolean isSendPresence() {
		return config.isSendPresence();
	}

	/**
	 * Get a map of all packet interceptors for sending packets of this
	 * connection.
	 * 
	 * @return a map of all packet interceptors for sending packets.
	 */
	protected Map<PacketInterceptor, InterceptorWrapper> getPacketInterceptors() {
		return interceptors;
	}

	/**
	 * Process interceptors. Interceptors may modify the packet that is about to
	 * be sent. Since the thread that requested to send the packet will invoke
	 * all interceptors, it is important that interceptors perform their work as
	 * soon as possible so that the thread does not remain blocked for a long
	 * period.
	 * 
	 * @param packet
	 *            the packet that is going to be sent to the server
	 */
	protected void firePacketInterceptors(Packet packet) {
		if (packet != null) {
			for (InterceptorWrapper interceptorWrapper : interceptors.values()) {
				interceptorWrapper.notifyListener(packet);
			}
		}
	}

	/**
	 * Initialize the {@link #debugger}. You can specify a customized
	 * {@link SmackDebugger} by setup the system property
	 * <code>smack.debuggerClass</code> to the implementation.
	 * 
	 * @throws IllegalStateException
	 *             if the reader or writer isn't yet initialized.
	 * @throws IllegalArgumentException
	 *             if the SmackDebugger can't be loaded.
	 */
	protected void initDebugger() {
		if (reader == null || writer == null) {
			throw new NullPointerException(
					"Reader or writer isn't initialized.");
		}
		// If debugging is enabled, we open a window and write out all network
		// traffic.
		if (config.isDebuggerEnabled()) {
			if (debugger == null) {
				// Detect the debugger class to use.
				String className = null;
				// Use try block since we may not have permission to get a
				// system
				// property (for example, when an applet).
				try {
					className = System.getProperty("smack.debuggerClass");
				} catch (Throwable t) {
					// Ignore.
				}
				Class<?> debuggerClass = null;
				if (className != null) {
					try {
						debuggerClass = Class.forName(className);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				if (debuggerClass == null) {
					try {
						debuggerClass = Class
								.forName("de.measite.smack.AndroidDebugger");
					} catch (Exception ex) {
						try {
							debuggerClass = Class
									.forName("org.jivesoftware.smack.debugger.ConsoleDebugger");
						} catch (Exception ex2) {
							ex2.printStackTrace();
						}
					}
				}
				// Create a new debugger instance. If an exception occurs then
				// disable the debugging
				// option
				try {
					Constructor<?> constructor = debuggerClass.getConstructor(
							Connection.class, Writer.class, Reader.class);
					debugger = (SmackDebugger) constructor.newInstance(this,
							writer, reader);
					reader = debugger.getReader();
					writer = debugger.getWriter();
				} catch (Exception e) {
					throw new IllegalArgumentException(
							"Can't initialize the configured debugger!", e);
				}
			} else {
				// Obtain new reader and writer from the existing debugger
				reader = debugger.newConnectionReader(reader);
				writer = debugger.newConnectionWriter(writer);
			}
		}

	}

	/**
	 * A wrapper class to associate a packet filter with a listener.
	 */
	protected static class ListenerWrapper {

		private PacketListener packetListener;
		private PacketFilter packetFilter;

		/**
		 * Create a class which associates a packet filter with a listener.
		 * 
		 * @param packetListener
		 *            the packet listener.
		 * @param packetFilter
		 *            the associated filter or null if it listen for all
		 *            packets.
		 */
		public ListenerWrapper(PacketListener packetListener,
				PacketFilter packetFilter) {
			this.packetListener = packetListener;
			this.packetFilter = packetFilter;
		}

		/**
		 * Notify and process the packet listener if the filter matches the
		 * packet.
		 * 
		 * @param packet
		 *            the packet which was sent or received.
		 */
		public void notifyListener(Packet packet) {
			if (packetFilter == null || packetFilter.accept(packet)) {
				packetListener.processPacket(packet);
			}
		}
	}

	/**
	 * A wrapper class to associate a packet filter with an interceptor.
	 */
	protected static class InterceptorWrapper {

		private PacketInterceptor packetInterceptor;
		private PacketFilter packetFilter;

		/**
		 * Create a class which associates a packet filter with an interceptor.
		 * 
		 * @param packetInterceptor
		 *            the interceptor.
		 * @param packetFilter
		 *            the associated filter or null if it intercepts all
		 *            packets.
		 */
		public InterceptorWrapper(PacketInterceptor packetInterceptor,
				PacketFilter packetFilter) {
			this.packetInterceptor = packetInterceptor;
			this.packetFilter = packetFilter;
		}

		public boolean equals(Object object) {
			if (object == null) {
				return false;
			}
			if (object instanceof InterceptorWrapper) {
				return ((InterceptorWrapper) object).packetInterceptor
						.equals(this.packetInterceptor);
			} else if (object instanceof PacketInterceptor) {
				return object.equals(this.packetInterceptor);
			}
			return false;
		}

		/**
		 * Notify and process the packet interceptor if the filter matches the
		 * packet.
		 * 
		 * @param packet
		 *            the packet which will be sent.
		 */
		public void notifyListener(Packet packet) {
			if (packetFilter == null || packetFilter.accept(packet)) {
				packetInterceptor.interceptPacket(packet);
			}
		}
	}
}
